package edu.hawaii.ics314.ical.model;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


/**
 * Defines the length of time between sets of time.
 * 
 * @author 
 *
 */
public class TimeSpan implements Comparable<TimeSpan> {
	
	private final DateTime START_TIME;
	
	private final DateTime END_TIME;
	
	/**
	 * Constructor that creates a new <code>TimeSpan</code> and defines the start and
	 * end times of an event.
	 * 
	 * @param startTime - the start time
	 * @param endTime - the end time
	 */
	public TimeSpan(DateTime startTime, DateTime endTime) {
		START_TIME = Objects.requireNonNull(startTime);
		END_TIME = Objects.requireNonNull(endTime);
		if(START_TIME.compareTo(END_TIME) > 0) {
			throw new IllegalArgumentException(String.format("start time (%s) must be equal to or before end time (%s)", 
					startTime.toString(), endTime.toString()));
		} else if(!START_TIME.hasDateComponent() || !END_TIME.hasDateComponent()) {
			throw new IllegalArgumentException("both DateTime instances must have date components");
		}
	}
	
	/**
	 * Returns the start time.
	 * 
	 * @return The start time
	 */
	public DateTime getStartDateTime() {
		return START_TIME;
	}
	
	/**
	 * Returns the end time.
	 * 
	 * @return The end time.
	 */
	public DateTime getEndDateTime() {
		return END_TIME;
	}
	
	/**
	 * Returns <code>true</code> if <code>other</code> is within this <code>TimeSpan</code>.
	 * 
	 * @param other - The <code>TimeSpan</code> being compared.
	 * @return <code>true</code> if <code>other</code> is within this <code>TimeSpan</code>, 
	 * <code>false</code> otherwise.
	 */
	public boolean contains(DateTime other) {
		return (START_TIME.compareTo(other) <= 0) && (END_TIME.compareTo(other) >= 0);
	}
	
	/**
	 * Returns <code>true</code> if <code>other</code> is within this <code>TimeSpan</code>.
	 * 
	 * @param other - The <code>TimeSpan</code> being compared.
	 * @return <code>true</code> if <code>other</code> is within this <code>TimeSpan</code>, 
	 * <code>false</code> otherwise.
	 */
	public boolean contains(TimeSpan other) {
		return contains(other.getStartDateTime()) && contains(other.getEndDateTime());
	}
	
	/**
	 * Returns if <code>other</code> overlaps with <code>TimeSpan</code>.
	 * 
	 * @param other - The <code>TimeSpan</code> being compared.
	 * @return If <code>other</code> overlaps with <code>TimeSpan</code>.
	 */
	public boolean overlaps(TimeSpan other) {
		return contains(other.getStartDateTime()) || contains(other.getEndDateTime());
	}
	
	/**
	 * Determines the total amount of time two events share. Note that the two event need not 
	 * overlap.
	 * 
	 * @param other - the <code>TimeSpan</code> which with this <code>TimeSpan</code> will be unioned.
	 * @return The total time two events share.
	 */
	public TimeSpan union(TimeSpan other) {
		Objects.requireNonNull(other);
		// Take the span that covers both time spans.
		DateTime start = (START_TIME.compareTo(other.START_TIME) <= 0 ? START_TIME : other.START_TIME); // Math.max()
		DateTime end = (END_TIME.compareTo(other.END_TIME) >= 0 ? END_TIME : other.END_TIME);			// Math.min()
		return new TimeSpan(start, end);
	}
	
	/**
	 * The amount of time that two events overlap.
	 * 
	 * @param other - the second event's start and end time.
	 * @return The amount of time two events overlap.
	 **/
	public TimeSpan intersection(TimeSpan other) {
		Objects.requireNonNull(other);
		// Check for intersection.
		if(contains(other)) {
			return other;
		} else if(other.contains(this)) {
			return this;
		} else if(overlaps(other)) {
			if(contains(other.getStartDateTime())) {
				return new TimeSpan(other.getStartDateTime(), END_TIME);
			} else {
				return new TimeSpan(START_TIME, other.getEndDateTime());
			}
		} else {
			return null;
		}
	}
	
	/**
	 * Returns this <code>TimeSpan</code> excluding the specified <code>TimeSpan</code>.
	 * 
	 * @param other - the second event's start and end time
	 * @return The time difference.
	 */
	public TimeSpan[] difference(TimeSpan other) {
		TimeSpan intersection = intersection(other);
		if(intersection == null) { 							// No intersection.
			return new TimeSpan[] {this};
		} else if(this.equals(intersection)) { 				// Time spans identical.
			return new TimeSpan[0];
		} else if(START_TIME.equals(other.START_TIME)) { 	// Intersection aligned with start of this time span.
			return new TimeSpan[] {new TimeSpan(intersection.END_TIME, END_TIME)};
		} else if(END_TIME.equals(other.END_TIME)) { 		// Intersection aligned with end of this time span.
			return new TimeSpan[] {new TimeSpan(START_TIME, intersection.START_TIME)};
		} else { 											// Intersection in the middle of this time span. 
			if(START_TIME.compareTo(other.START_TIME) <= 0) { 	// This time span precedes other one. 
				return new TimeSpan[] {
						new TimeSpan(START_TIME, intersection.START_TIME),
						new TimeSpan(intersection.END_TIME, other.END_TIME)
					};
			} else { 											// Other time span precedes this one. 
				return new TimeSpan[] {
						new TimeSpan(other.START_TIME, intersection.START_TIME),
						new TimeSpan(intersection.END_TIME, END_TIME)
					};
			}
		}
	}
	
	/**
	 * Returns the duration of this <code>TimeSpan</code> in the specified <code>TimeUnit</code>.
	 * 
	 * @param unit - The specified <code>TimeUnit</code>.
	 * @return The duration of the <code>TimeSpan</code> is. 
	 */
	public long toLong(TimeUnit unit) {
		return END_TIME.toLong(unit) - START_TIME.toLong(unit);
	}
	
	@Override
	public int compareTo(TimeSpan other) {
		return START_TIME.compareTo(other.getStartDateTime());
	}
	
	@Override
	public boolean equals(Object other) {
		if(other instanceof TimeSpan) {
			TimeSpan tsOther = (TimeSpan) other;
			return START_TIME.equals(tsOther.START_TIME) && END_TIME.equals(tsOther.END_TIME);
		} else {
			return false;
		}
	}
	
	@Override
	public String toString() {
		return "[" + START_TIME.toString() + "-->" + END_TIME.toString() + "]";
	}

}
